<!--
  Copyright 2008 Google Inc.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
  
  @author Brad Neuberg
  
  The slurp method and implementation is taken from Brian Dunnington's
  Vortex library
    (http://geekycodestuff.blogspot.com/2007/09/vortexoffline-demo-app.html),
  which is itself taken from Brad Neuberg's Dojo Offline
    (http://dojotoolkit.org/offline).
  Both are under the BSD license.
-->

<html>
  <head>
    <script>
      window.onload = function() {
        // write out the bookmarklet
        var a = document.getElementById('bookmarklet');
        a.href = toBookmarklet(bookmarklet);
      }
      
      /**
        Generates a bookmarklet that will inline this
        object and cause it to be run.
      */
      function toBookmarklet(f) {
        // enumerate the functions
        var allFuncs = [];
        for (var i in bookmarklet) {
          allFuncs.push(i + ': ' + bookmarklet[i]);
        }
        
        // stringify them and put commas between them
        var s = allFuncs.join(',');
        
        // remove trailing comma
        if (s.charAt(s.length - 1) == ',') {
          s = s.substring(0, s.length - 2);
        }
        
        // remove new lines
        s = s.replace(/\n/g, '');
        
        // turn into an object literal
        s = '{' + s + '}';
        
        // wrap as bookmarklet
        s = 'javascript:(function(){var f = ' + s + '; f.run(f);})()';
        
        return s;
      }
      
      /**
        We stringify this object into a bookmarklet. Doing it in 
        this form rather than a javascript: URL makes it much more
        maintainable and easier to read.
      */
      var bookmarklet = {
        /** 
          For testing -- mock value for window.location.
          Must have same properties as window.location, such as .href,
          .protocol, etc.
        */
        testLocation_: null,
        
        /**
          External entry point to actually execute this
          bookmarklet. 
        */
        run: function(self) {
          var files = self.slurp_(self);
          var manifest = self.generateManifest_(self, files);
          self.displayResults_(self, manifest);
        },
        
        /* Slurps the page for resources to make offline, such as images. */
        slurp_: function(self) { // see copyright info at top
          var elements;
          var e;
          var i;
          var files = self.newSet_();

          // this page
          var loc = window.location || self.testLocation_;
          files.push(loc.href);
          
          // links
          elements = document.getElementsByTagName('a');
          for (i = 0; i < elements.length; i++) {
            e = elements[i];
            var href = e.href;
            if (href) {
              href = href.replace(/^\s+/, '');
              href = href.replace(/\s+$/, '');
              href = href.replace(/#.*$/, '');
              if (/^(?:javascript:)|(?:mailto:)/i.test(href)) {
                continue;
              }
            }
            
            if (href) {
              files.push(href);
            }
          }

          // referenced scripts
          elements = document.getElementsByTagName('script');
          for (i = 0; i < elements.length; i++) {
            e = elements[i];
            if (e.src && e.src != '') {
              // make sure not to pull in pubtools-gears.js
              if (/pubtools\-gears\.js/.test(e.src)) {
                continue;
              }
              
              files.push(e.src);
            }
          }

          // referenced inline images
          elements = document.getElementsByTagName('img');
          for (i = 0; i < elements.length; i++) {
            e = elements[i];
            if (e.src && e.src != '') {
              files.push(e.src);
            }
          }

          // referenced stylesheets
          elements = document.getElementsByTagName('link');
          for (i = 0; i < elements.length; i++) {
            e = elements[i];
            if (e.rel && e.rel.toLowerCase() == 'stylesheet') {
              files.push(e.getAttribute('href'));
            }
          }

          // @import and url()
          if (document.styleSheets && document.styleSheets.length) {
            for (var i = 0; i < document.styleSheets.length; i++) {
              self.parseStylesheets_(self, document.styleSheets[i], files);
            }
          }
          
          // Flash OBJECTs
          elements = document.getElementsByTagName('object');
          var kFLASH_CLASS_ID = 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000';
          for (var i = 0; i < elements.length; i++) {
            var classid = elements[i].getAttribute('classid');
            if (classid == kFLASH_CLASS_ID 
                || classid == kFLASH_CLASS_ID.toLowerCase()) {
              // we have a Flash object -- get its PARAM children
              var params = elements[i].childNodes;
              for (var j = 0; j < params.length; j++) {
                var paramName = params[j].nodeName;
                if (paramName && paramName.toLowerCase() == 'param') {
                  if (params[j].getAttribute('name') == 'movie') {
                    var url = params[j].getAttribute('value');
                    files.push(url);
                    break; // done with this OBJECT
                  }
                }
              }
            }
          }

          return self.filterURLs_(self, files.toList());
        },
        
        parseStylesheets_: function(self, sheet, files){
          try {
            // handle @import recursively (IE only)
            if (sheet.imports && sheet.imports.length) {
              for (var i = 0; i < sheet.imports.length; i++) {
                files.push(sheet.imports[i].href);
                self.parseStylesheets_(sheet.imports[i], files);
              }
            }

            var rules = (sheet.cssRules || sheet.rules);
            if (rules && rules.length) {
              for (var r = 0; r < rules.length; r++) {
                var rule = rules[r];

                // handle @imports recursively (FF)
                if (rule.styleSheet) {
                  self.parseStylesheets_(rule.styleSheet, files);
                }

                var text = (rule.cssText || rule.style.cssText);
                if (text) {
                  var matches = text.match(/url\(\s*([^\) ]*)\s*\)/i);
                  if (matches) {
                    for (var j = 1; j < matches.length; j++) {
                      files.push(matches[j]);
                    }
                  }
                }
              }
            } // end if
          } catch(err) {
          }
        },
        
        filterURLs_: function(self, files) {
          var r = [];
          var bad = [];
          
          // ensure URLs are either relative, absolute, or follow the
          // same domain policy
          
          for (var i = 0; i < files.length; i++) {
            // parse the URI, which possibly has no scheme/host/port
            var url = files[i];
            var m = url.match(/^\s*(?:(\w+):\/\/([^\/:]+)?(?::(\d+))?)?(.*)$/);
            var scheme = m[1];
            var host = m[2];
            var port = m[3] || "80";
            var path = m[4];

            // determine what our page's scheme, host, and port are
            var loc = window.location || self.testLocation_;
            var pageScheme = loc.protocol;
            // some browsers have a trailing :
            if (pageScheme && pageScheme.charAt(pageScheme.length - 1) == ":") {
              pageScheme = pageScheme.substring(0, pageScheme.length - 1);
            }
            var pageHost = loc.hostname;
            var pagePort = loc.port || "80";

            // fully expanded already?
            if (scheme) {
              // does our scheme/host/port follow the same-domain
              // security policy?
              if(scheme == pageScheme && host == pageHost && port == pagePort) {
                r.push(path); // we're golden; just take relative part
                continue;
              }
            } else {
              r.push(path); // absolute or relative URL; just take relative part
              continue;
            }
            
            bad.push(url);
          }
          
          if (bad.length) {
            if(typeof console != 'undefined') {
              console.debug('The following URLs were not added to generated '
                              + 'manifest file because they are not from the '
                              + 'same protocol/host/port as this page:');
              for (var i = 0; i < bad.length; i++) {
                console.debug(bad[i]);
              }
            }
          }
      
          return r;
        },
        
        generateManifest_: function(self, files) {
          // we will just replace our %variable%'s with the actual
          // values
          var template = 
            '{\n'
             +  '\t"betaManifestVersion": 1,\n'
             +  '\t"version": "%myVersion%",\n'
             +  '\t"entries": [\n'
             +      '%entries%'
             +  '\t]\n'
             + '}\n';
             
          // template for each of the URL entries that we will write out
          // as an array
          var urlTemplate = '\t\t{ "url": "%url%" },\n';
          
          // just make the version be a millisecond timestamp
          var myVersion = 'millisecond-timestamp-' + new Date().getTime();
          template = template.replace('%myVersion%', myVersion);
          
          // handle our list of file URLs
          var entries = [];
          for (var i = 0; i < files.length; i++) {
            entries.push(urlTemplate.replace('%url%', files[i]));
          }
          entries = entries.join('');
          
          // removing trailing comma
          if (entries.length && entries.charAt(entries.length - 2) == ',') {
            entries = entries.substring(0, entries.length - 2);
            entries += '\n';
          }
          
          var r = template.replace('%entries%', entries);
          
          return r;
        },
        
        displayResults_: function(self, manifest) {
          var div = document.createElement('div');
          div.style.position = 'absolute';
          div.style.top = '50px';
          div.style.left = '50px';
          div.style.zIndex = 10;
          div.style.border = '1px solid black';
          div.style.backgroundColor = 'lightgray';
          div.style.padding = '9px';
          div.style.fontSize = '12pt';
          
          var container = document.createElement('div');
          container.style.width = '60em';
          div.appendChild(container);
          
          var instructions = document.createElement('p');
          instructions.style.width = '100%';
          instructions.style.marginBottom = '5px';
          instructions.innerHTML = 'Copy the following text into a new '
                                        + 'file and place it on your web '
                                        + 'server with a filename such '
                                        + 'as manifest.js. In your HTML, '
                                        + 'include the script file '
                                        + 'pubtools-gears.js, and on your '
                                        + 'HTML element add the attribute '
                                        + '"manifest" with the value being '
                                        + 'the location and filename of '
                                        + 'the manifest file generated below.';
          container.appendChild(instructions);
          
          var area = document.createElement('textarea');
          area.spellcheck = false;
          area.style.width = '98%';
          area.style.height = '30em';
          area.style.padding = '4px';
          area.appendChild(document.createTextNode(manifest));
          container.appendChild(area);
          
          var close = document.createElement('button');
          close.innerHTML = 'Close';
          close.style.display = 'block';
          close.style.marginTop = '7px';
          close.style.marginLeft = '5px';
          close.onclick = function() {
            div.parentNode.removeChild(div); 
          }
          div.appendChild(close);
          
          var body = document.getElementsByTagName('body')[0];
          body.appendChild(div);
          
          // jump to the dialog box in case the page is scrolled down
          var x = y = 0;
          var elem = div;
          while (elem) {
            x += elem.offsetLeft;
            y += elem.offsetTop;
            elem = elem.offsetParent;
          }

          window.scrollTo(x,y);
        },
        
        /** Simple set implementation to prevent duplicate URLs. */
        newSet_: function() {
          var Set = function() {
            this.list_ = [];
          }
          
          Set.prototype.push = function(entry) {
            for (var i = 0; i < this.list_.length; i++) {
              if (this.list_[i] == entry) {
                return;
              }
            }
            
            this.list_.push(entry);
            this.list_.sort();
          }
          
          Set.prototype.toList = function() {
            return this.list_;
          }
          
          return new Set();
        }
      }
      
      /*
        Print debug/log messages on browsers without Firebug. 
      */
      if (typeof console == 'undefined' || !console.debug) {
        console = {};
        console.debug = function(msg) {
          var e = document.createElement('div');
          e.style.backgroundColor = 'white';
          e.style.color = 'black';
          e.appendChild(document.createTextNode(msg));
          var body = document.getElementsByTagName('body');
          if (!body || !body.length || body.length == 0) {
            return;
          }
          body[0].appendChild(e);
        }
      }
    </script>
  </head>
  
  <body>
    <h1>Manifest Bookmarklet</h1>
    
    <p>This page contains a bookmarklet that makes it easy to
      create Gears manifest files without having to write them by hand. It
      will automatically identify all the resources you want to make available
      offline, such as images, stylesheets, etc. and then generate a
      manifest file for you.</p>
      
    <p>To use, drag the following bookmarklet to your links toolbar. Then, 
      navigate to the web page that you would like to offline enable. Once
      pressed, this bookmarklet will scan the page, identify all the resources
      that you might want to have offline, and automatically generate
      a Gears offline manifest file for you. This manifest file will 
      appear in a text area on your screen -- you can now edit this file, then
      copy and save it into a new file, such as <i>manifest.js</i>.</p>
      
    <p>Note that this script only works on Firefox and Safari due to
      limitations in Internet Explorer on the length of bookmarklets.</p>
      
    <div><a id="bookmarklet" href="#">Generate Gears Manifest</a></div> 
  </body>
</html>